﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace FetionLite
{
    /// <summary>
    /// An singleton Fetion wrapper for libfetion p/invoke
    /// </summary>
    public class Fetion : IDisposable
    {
        private static volatile Fetion _instance;
        private static object _syncRoot = new object();
        private static FxCallBack _loginEventDelegate;
        private static FxCallBack _fxSysEventDelegate;
        
        [DllImport("LibFx")]
        private static extern bool fx_init();

        [DllImport("LibFx")]
        private static extern void fx_terminate();

        [DllImport("LibFx")]
        private static extern bool fx_set_serve_address([In]string serverAddress);

        [DllImport("LibFx")]
        private static extern bool fs_login(string username, string pwd);

        [DllImport("LibFx")]
        private static extern int fx_login(byte[] mobile, byte[] pwd, FxCallBack callback, ref IntPtr arg);

        [DllImport("LibFx")]
        private static extern void fx_loginout();

        [DllImport("LibFx")]
        private static extern void fx_set_system_msg_cb(FxCallBack callback, ref IntPtr arg);

        [DllImport("LibFx")]
        private static extern bool fs_send_sms_to_self(byte[] mesage);

        [DllImport("LibFx")]
        private static extern IntPtr fx_get_group();

        [DllImport("LibFx")]
        private static extern IntPtr fx_get_first_account();

        [DllImport("LibFx")]
        private static extern IntPtr fx_get_next_account(IntPtr fetionAccount);

        [DllImport("LibFx")]
        private static extern void fx_update_account_info_by_id(int id);

        [DllImport("LibFx")]
        private static extern IntPtr fx_get_account_by_id(int id);

        [DllImport("LibFx")]
        private static extern int fx_get_account_group_id(IntPtr account);

        [DllImport("LibFx")]
        private static extern IntPtr fx_get_msg(int accountId);

        [DllImport("LibFx")]
        private static extern void fx_destroy_msg(IntPtr msg);

        [DllImport("LibFx")]
        private static extern bool fx_is_pc_user_by_id(int id);

        [DllImport("LibFx")]
        private static extern int fx_begin_dialog(int id, IntPtr callback, IntPtr arg);

        [DllImport("LibFx")]
        private static extern int fx_dialog_send(int id, byte[] message, IntPtr callback, IntPtr arg);

        [DllImport("LibFx")]
        private static extern void fx_end_dialog(int id);

        [DllImport("LibFx")]
        private static extern bool fx_set_serve_address(byte[] serverAddress);

        [DllImport("LibFx")]
        private static extern IntPtr fx_get_serve_address();

        [DllImport("LibFx")]
        private static extern IntPtr fx_msg_no_format(IntPtr formattedMessage);

        [DllImport("LibFx")]
        private static extern int fx_send_sms(int id, byte[] message, IntPtr callback, IntPtr arg);

        [DllImport("LibFx")]
        private static extern IntPtr fx_get_account_show_name(IntPtr account, bool needImpresa);

        [DllImport("LibFx")]
        private static extern bool fx_is_on_line_by_id(int id);

        [DllImport("LibFx")]
        private static extern int fx_get_online_status_by_account(IntPtr accountPtr);

        [DllImport("LibFx")]
        private static extern int fx_get_online_status_by_id(int id);

        public delegate void FxCallBack(int message, int wParam, int lParam, IntPtr arg);
        public delegate void CallMsg(string msg);

        private bool _loginSuccessed = false;
        private IntPtr _conHandle = new IntPtr();

        private Fetion()
        {
            if (!fx_init())
                throw new Exception("初始化libfetion失败！");
        }

        public static Fetion Instance
        {
            get
            {
                if (_instance == null)
                {
                    lock (_syncRoot)
                    {
                        if (_instance == null)
                            _instance = new Fetion();
                    }
                }

                return _instance;
            }
        }

        public string Mobile
        {
            get;
            set;
        }

        public string Password
        {
            get;
            set;
        }

        public void Login(string mobile, string pwd)
        {
            this.Mobile = mobile;
            this.Password = pwd;

            Guid netGuid = new Guid();
            ConnectionManager.ConnMgrMapURL("http://221.130.45.208:8080", ref netGuid, 0);
            ConnectionManager.ConnMgrConnectionInfo conInfo = new ConnectionManager.ConnMgrConnectionInfo(netGuid);
            ConnectionManager.ConnMgrStatus conStatus = new ConnectionManager.ConnMgrStatus();
            ConnectionManager.ConnMgrEstablishConnectionSync(conInfo, ref _conHandle, 30000, ref conStatus);

            if (conStatus != ConnectionManager.ConnMgrStatus.Connected)
                throw new Exception("连接网络失败！");

            _loginEventDelegate = new FxCallBack(LoginEventListener);
            IntPtr pt = new IntPtr();            
            fx_login(StringToUTF8Bytes(mobile), StringToUTF8Bytes(pwd), _loginEventDelegate, ref pt);
        }

        public delegate void LoginStateChangedDelegate(LoginState loginState);
        public event LoginStateChangedDelegate OnLoginStateChanged;

        private void LoginEventListener(int message, int wParam, int lParam, IntPtr arg)
        {
            if (message == (int)LoginState.FX_LOGIN_OK)
            {
                _loginSuccessed = true;

                //register fetion system event call back
                _fxSysEventDelegate = new FxCallBack(FxEventListener);
                IntPtr pt = new IntPtr();
                fx_set_system_msg_cb(_fxSysEventDelegate, ref pt);
            }

            if (OnLoginStateChanged != null)
                OnLoginStateChanged((LoginState)message);
        }

        public delegate void NewMessageDeletegate(FetionAccount fromAccount, FetionMsg msg);
        public event NewMessageDeletegate OnNewMessage;

        public delegate void AccountOnlineStatusChangedDelegate(FetionAccount account, FetionUserStatus status);
        public event AccountOnlineStatusChangedDelegate OnAccountOnlineStatusChanged;

        public delegate void AccountUpdateOKDeletegate(FetionAccount account);
        public event AccountUpdateOKDeletegate OnAccountUpdateOK;

        private void FxEventListener(int message, int wParam, int lParam, IntPtr arg)
        {
            FxSysMessage sysMsg = (FxSysMessage)message;
            FetionAccount account;

            switch (sysMsg)
            {
                case FxSysMessage.FX_NEW_MESSAGE:

                    if (lParam == 10000)
                    {
                        account = new FetionAccount();
                        account.Personal = new FetionPersonal();
                        account.Personal.NickName = "系统消息";
                        account.ShowName = "系统消息";
                        account.Id = 10000;
                        account.GroupId = 0;
                    }
                    else
                        account = GetAccountById(lParam);

                    IntPtr msgPtr = fx_get_msg(lParam);                    
                    FetionMsg msg = PtrToMsg(msgPtr);
                    fx_destroy_msg(msgPtr);

                    if (OnNewMessage != null)
                        OnNewMessage(account, msg);

                    break;

                case FxSysMessage.FX_ACCOUNT_UPDATA_OK:

                    account = GetAccountById(lParam);

                    if (OnAccountUpdateOK != null)
                        OnAccountUpdateOK(account);

                    break;

                case (FxSysMessage)FetionUserStatus.FX_STATUS_ONLINE:
                case (FxSysMessage)FetionUserStatus.FX_STATUS_OFFLINE:
                case (FxSysMessage)FetionUserStatus.FX_STATUS_BUSY:
                case (FxSysMessage)FetionUserStatus.FX_STATUS_AWAY:
                case (FxSysMessage)FetionUserStatus.FX_STATUS_MEETING:
                case (FxSysMessage)FetionUserStatus.FX_STATUS_PHONE:
                case (FxSysMessage)FetionUserStatus.FX_STATUS_DINNER:
                case (FxSysMessage)FetionUserStatus.FX_STATUS_EXTENDED_AWAY:
                case (FxSysMessage)FetionUserStatus.FX_STATUS_NUM_PRIMITIVES:
                case (FxSysMessage)FetionUserStatus.FX_STATUS_SMSEXTENED:
                    //不是当前用户则触发好友上线事件
                    if (lParam != int.Parse(this.Mobile))
                    {
                        account = GetAccountById(lParam);

                        if (OnAccountOnlineStatusChanged != null)
                            OnAccountOnlineStatusChanged(account, (FetionUserStatus)sysMsg);
                    }

                    break;

            }
        }

        public List<FetionGroup> GetGroupList()
        {
            List<FetionGroup> groups = new List<FetionGroup>();

            IntPtr groupListPtr = fx_get_group();

            while (groupListPtr != IntPtr.Zero)
            {
                IntPtr groupPtr = Marshal.ReadIntPtr(groupListPtr);
                FetionGroup group = PtrToGroup(groupPtr);
                groups.Add(group);

                groupListPtr = ReadIntPtr(groupListPtr, 4);
            }

            return groups;
        }

        public List<FetionAccount> GetAccountList()
        {
            List<FetionAccount> accouts = new List<FetionAccount>();

            IntPtr accountsPtr = fx_get_first_account();

            while (accountsPtr != IntPtr.Zero)
            {
                FetionAccount account = PtrToAccount(accountsPtr);

                account.GroupId = fx_get_account_group_id(accountsPtr);

                IntPtr showNamePtr = fx_get_account_show_name(accountsPtr, false);
                account.ShowName = ReadStringUTF8(showNamePtr);
                Marshal.FreeHGlobal(showNamePtr);

                accouts.Add(account);

                accountsPtr = fx_get_next_account(accountsPtr);
            }

            return accouts;
        }

        public FetionAccount GetAccountById(int id)
        {
            IntPtr ptr = fx_get_account_by_id(id);

            FetionAccount account = null;

            if (ptr != IntPtr.Zero)
            {
                account = PtrToAccount(ptr);
                account.GroupId = fx_get_account_group_id(ptr);
                account.OnlineStatus = (FetionUserStatus)fx_get_online_status_by_account(ptr);

                IntPtr showNamePtr = fx_get_account_show_name(ptr, false);
                account.ShowName = ReadStringUTF8(showNamePtr);
                Marshal.FreeHGlobal(showNamePtr);
            }

            return account;
        }

        public void UpdateAccountById(int id)
        {
            fx_update_account_info_by_id(id);
        }

        public bool IsPCUser(int id)
        {
            return fx_is_pc_user_by_id(id);
        }

        public int BeginDialog(int id)
        {
            return fx_begin_dialog(id, IntPtr.Zero, IntPtr.Zero);
        }

        public void EndDialog(int id)
        {
            fx_end_dialog(id);
        }

        public int SendDialogMessage(int id, string message)
        {
            return fx_dialog_send(id, StringToUTF8Bytes(message), IntPtr.Zero, IntPtr.Zero);
        }

        public int SendSMSMessage(int id, string message)
        {
            return fx_send_sms(id, StringToUTF8Bytes(message), IntPtr.Zero, IntPtr.Zero);
        }

        public bool SetServerAddress(string address)
        {
            return fx_set_serve_address(StringToUTF8Bytes(address));
        }

        public string GetServerAddress()
        {
            IntPtr serverAddsPtr = fx_get_serve_address();
            
            string serverAdds = ReadStringUTF8(serverAddsPtr);

            Marshal.FreeHGlobal(serverAddsPtr);

            return serverAdds;
        }

        public bool IsOnlineById(int id)
        {
            return fx_is_on_line_by_id(id);
        }

        private FetionGroup PtrToGroup(IntPtr ptr)
        {
            if (ptr != IntPtr.Zero)
            {
                FetionGroup group = new FetionGroup();

                group.Id = Marshal.ReadInt32(ptr);
                group.Name = PtrToStringUTF8(ptr, 4);

                return group;
            }
            else
                return null;
        }

        private FetionAccount PtrToAccount(IntPtr ptr)
        {
            if (ptr != IntPtr.Zero)
            {
                FetionAccount account = new FetionAccount();

                account.Id = Marshal.ReadInt32(ptr);
                account.Uri = PtrToStringUTF8(ptr, 4);
                account.LocalName = PtrToStringUTF8(ptr, 8);
                account.BuddyLists = PtrToStringUTF8(ptr, 12);
                account.GroupId = Marshal.ReadInt32(ptr, 16);
                account.RelationStatus = Marshal.ReadInt32(ptr, 20);
                account.OnlineNotify = Marshal.ReadInt32(ptr, 24);
                account.StatusCode = Marshal.ReadInt32(ptr, 28);
                account.OnlineStatus = (FetionUserStatus)Marshal.ReadInt32(ptr, 32);
                account.PortraitCRC = Marshal.ReadInt32(ptr, 36);

                IntPtr personalPtr = ReadIntPtr(ptr, 40);
                account.Personal = PtrToPersonal(personalPtr);

                account.UserType = (FetionUserType)Marshal.ReadInt32(ptr, 44);
                account.DeviceType = PtrToStringUTF8(ptr, 48);

                return account;
            }
            else
                return null;
        }

        private FetionPersonal PtrToPersonal(IntPtr ptr)
        {
            if (ptr != IntPtr.Zero)
            {
                FetionPersonal personal = new FetionPersonal();

                personal.NickName = PtrToStringUTF8(ptr, 0);
                personal.Impresa = PtrToStringUTF8(ptr, 4);
                personal.Gender = Marshal.ReadInt32(ptr, 8);
                personal.Nation = PtrToStringUTF8(ptr, 12);
                personal.Province = PtrToStringUTF8(ptr, 16);
                personal.City = Marshal.ReadInt32(ptr, 20);
                personal.IvrEnabled = Marshal.ReadInt32(ptr, 24);
                personal.PortraitCRC = Marshal.ReadInt32(ptr, 28);
                personal.Provisoning = Marshal.ReadInt32(ptr, 32);
                personal.MobileNo = PtrToStringUTF8(ptr, 36);
                personal.Name = PtrToStringUTF8(ptr, 40);
                personal.Birthday = PtrToStringUTF8(ptr, 44);
                personal.BirthdayValid = Marshal.ReadInt32(ptr, 48);
                personal.LunarAnimal = Marshal.ReadInt32(ptr, 52);
                personal.Horoscope = Marshal.ReadInt32(ptr, 56);
                personal.Profile = PtrToStringUTF8(ptr, 60);
                personal.BloodType = Marshal.ReadInt32(ptr, 64);
                personal.Occupation = PtrToStringUTF8(ptr, 68);
                personal.Hobby = PtrToStringUTF8(ptr, 72);
                personal.PersonalEmail = PtrToStringUTF8(ptr, 76);
                personal.WorkEmail = PtrToStringUTF8(ptr, 80);
                personal.OtherEmail = PtrToStringUTF8(ptr, 84);
                personal.PrimaryEmail = Marshal.ReadInt32(ptr, 88);
                personal.JobTitle = PtrToStringUTF8(ptr, 92);
                personal.HomePhone = PtrToStringUTF8(ptr, 96);
                personal.WorkPhone = PtrToStringUTF8(ptr, 100);
                personal.OtherPhone = PtrToStringUTF8(ptr, 104);
                personal.Company = PtrToStringUTF8(ptr, 108);
                personal.CompanyWebSite = PtrToStringUTF8(ptr, 112);
                personal.MatchEnabled = Marshal.ReadInt32(ptr, 116);
                personal.RefuseSMSDay = Marshal.ReadInt32(ptr, 120);

                return personal;
            }
            else
                return null;
        }

        /// <summary>
        /// 将ptr指向的地址中的数据转换为FetionMsg，注意，FetionMsg包含libfetion struct Fetion_Msg中
        /// 没有的Field，PlainMessage，本方法已经通过fx_msg_no_format为PlainMessage属性赋值
        /// </summary>
        /// <param name="ptr"></param>
        /// <returns></returns>
        private FetionMsg PtrToMsg(IntPtr ptr)
        {
            FetionMsg msg = null;

            if (ptr != IntPtr.Zero)
            {
                msg = new FetionMsg();

                msg.AccountId = Marshal.ReadInt32(ptr);
                msg.Message = PtrToStringUTF8(ptr, 4);                
                msg.MessageFormat = PtrToStringUTF8(ptr, 8);
                msg.MessageTime = PtrToStringUTF8(ptr, 12);
                msg.QunId = Marshal.ReadInt32(ptr, 16);

                //extended property PlainMessage
                IntPtr formattedMagPtr = ReadIntPtr(ptr, 4);
                IntPtr plainMessagePtr = fx_msg_no_format(formattedMagPtr);
                msg.PlainMessage = ReadStringUTF8(plainMessagePtr);
                Marshal.FreeHGlobal(plainMessagePtr);
            }

            return msg;
        }

        private IntPtr ReadIntPtr(IntPtr baseAddress, int offset)
        {
            int baseAddressTmp = baseAddress.ToInt32();
            IntPtr destPtr = new IntPtr(baseAddressTmp + offset);
            return Marshal.ReadIntPtr(destPtr);
        }

        /// <summary>
        /// 从指定地址直接读取utf8编码的字符串并转换为unicode字符串传出
        /// </summary>
        /// <param name="utf8StrPtr"></param>
        /// <returns></returns>
        private string ReadStringUTF8(IntPtr utf8StrPtr)
        {
            if (utf8StrPtr == IntPtr.Zero)
                return string.Empty;

            List<byte> byteList = new List<byte>(256);
            int strOffset = 0;

            byte b = Marshal.ReadByte(utf8StrPtr, strOffset);
            while (b != 0)
            {
                byteList.Add(b);
                strOffset++;
                b = Marshal.ReadByte(utf8StrPtr, strOffset);
            }

            byte[] utf8Bytes = byteList.ToArray();
            byte[] unicodeBytes = Encoding.Convert(Encoding.UTF8, Encoding.Unicode, utf8Bytes);

            return Encoding.Unicode.GetString(unicodeBytes, 0, unicodeBytes.Length);
        }

        private string PtrToStringUTF8(IntPtr baseAddress, int offset)
        {
            IntPtr utf8StrPtr = ReadIntPtr(baseAddress, offset);

            if (utf8StrPtr == IntPtr.Zero)
                return string.Empty;

            List<byte> byteList = new List<byte>(256);
            int strOffset = 0;

            byte b = Marshal.ReadByte(utf8StrPtr, strOffset);
            while (b != 0)
            {
                byteList.Add(b);
                strOffset++;
                b = Marshal.ReadByte(utf8StrPtr, strOffset);
            }

            byte[] utf8Bytes = byteList.ToArray();
            byte[] unicodeBytes = Encoding.Convert(Encoding.UTF8, Encoding.Unicode, utf8Bytes);

            return Encoding.Unicode.GetString(unicodeBytes, 0, unicodeBytes.Length);
        }

        /// <summary>
        /// libfetion use UTF-8 as input/output string, 
        /// we have to convert .net unicode string to utf-8 byte array which is null-terminated
        /// for p/invoke
        /// </summary>
        /// <param name="sourceString"></param>
        /// <returns></returns>
        private byte[] StringToUTF8Bytes(string sourceString)
        {
            byte[] unicodeBytes = Encoding.Unicode.GetBytes(sourceString);
            byte[] utf8Bytes = Encoding.Convert(Encoding.Unicode, Encoding.UTF8, unicodeBytes);
            byte[] destBytes = new byte[utf8Bytes.Length + 1];

            Array.Copy(utf8Bytes, destBytes, utf8Bytes.Length);

            return destBytes;
        }

        public void Dispose()
        {
            lock (_syncRoot)
            {
                if (_loginSuccessed)
                    fx_loginout();

                fx_terminate();

                if (_conHandle != IntPtr.Zero)
                    ConnectionManager.ConnMgrReleaseConnection(_conHandle, 0);

                _instance = null;
            }
        }

    }
}
